7장. 트랜잭션과 데이터 일관성
1. 하나의 작업처럼 보이지만
서비스에서는 여러 데이터 변경이
하나의 작업처럼 묶이는 경우가 많다.
예를 들어 주문을 생성하는 로직을 보면 다음과 같다.
await orderRepository.create(order);
await userRepository.decreasePoint(userId, amount);
이 코드는 자연스럽게 읽힌다.
- 주문을 생성하고
- 포인트를 차감한다
👉 하나의 작업처럼 보인다
2. 하지만 실제로는 두 개의 작업이다
문제는 이 두 작업이
서로 다른 DB 작업이라는 점이다.
만약 다음과 같은 상황이 발생하면 어떻게 될까?
await orderRepository.create(order); // 성공
await userRepository.decreasePoint(...); // 실패
👉 결과
- 주문은 생성됨
- 포인트는 차감되지 않음
3. 데이터가 깨지는 순간
이 상태는 비즈니스적으로 문제가 된다.
- 돈은 안 빠졌는데 주문은 완료됨
- 혹은 반대로 돈만 빠지고 주문 실패
👉 데이터 일관성이 깨진다
4. 그래서 트랜잭션이 필요하다
이 문제를 해결하기 위해 등장한 개념이
👉 트랜잭션(Transaction) 이다
트랜잭션은 여러 작업을 하나로 묶는다.
await db.transaction(async (trx) => {
await orderRepository.create(order, trx);
await userRepository.decreasePoint(userId, amount, trx);
});
👉 결과
- 둘 다 성공 → 반영
- 하나라도 실패 → 전체 롤백
5. 그런데 구조를 나누면 문제가 생긴다
6장에서 구조를 이렇게 나눴다.
Controller → Service → Repository
이 구조에서 트랜잭션은 어디에서 관리해야 할까?
6. 자연스럽게 잘못된 선택
처음에는 Repository에서 처리하고 싶어진다.
class OrderRepository {
async create(order) {
await db.transaction(async () => {
await db.query(...);
});
}
}
👉 하지만 이 구조는 문제가 있다
- 다른 작업과 묶을 수 없다
- 트랜잭션이 분리된다
👉 결국 의미가 없다
7. 해결은 Service에 있다
트랜잭션은 “데이터 저장”이 아니라
👉 “작업의 흐름”을 기준으로 묶어야 한다
그래서 Service에서 관리한다.
await db.transaction(async (trx) => {
await orderRepository.create(order, trx);
await userRepository.decreasePoint(userId, amount, trx);
});
👉 핵심
- Service → 전체 흐름 제어
- Repository → 실행만 담당
8. Repository는 트랜잭션을 전달받는다
class UserRepository {
async decreasePoint(userId, amount, trx?) {
return trx
? trx.query(...)
: db.query(...);
}
}
👉 트랜잭션이 있으면 사용
👉 없으면 기본 DB 사용
9. 핵심은 위치다
트랜잭션을 어디에 두느냐가 중요하다.
| 위치 | 결과 |
|---|---|
| Repository | 분리됨 (문제 발생) |
| Service | 하나로 묶임 (정상) |
10. 정리
이 장의 핵심은 단순하다.
👉 트랜잭션은 DB 기능이지만
👉 DB 레이어에서 관리하면 안 된다